/*
 *	This file is part of Transdroid <http://www.transdroid.org>
 *	
 *	Transdroid is free software: you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation, either version 3 of the License, or
 *	(at your option) any later version.
 *	
 *	Transdroid is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU General Public License for more details.
 *	
 *	You should have received a copy of the GNU General Public License
 *	along with Transdroid.  If not, see <http://www.gnu.org/licenses/>.
 *	
 */
package org.transdroid.daemon.Utorrent;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.IDaemonAdapter;
import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentDetails;
import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.TorrentStatus;
import org.transdroid.daemon.Label;
import org.transdroid.daemon.task.AddByFileTask;
import org.transdroid.daemon.task.AddByMagnetUrlTask;
import org.transdroid.daemon.task.AddByUrlTask;
import org.transdroid.daemon.task.DaemonTask;
import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult;
import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import org.transdroid.daemon.task.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
import org.transdroid.daemon.task.GetTorrentDetailsTask;
import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult;
import org.transdroid.daemon.task.RemoveTask;
import org.transdroid.daemon.task.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetFilePriorityTask;
import org.transdroid.daemon.task.SetLabelTask;
import org.transdroid.daemon.task.SetTrackersTask;
import org.transdroid.daemon.task.SetTransferRatesTask;
import org.transdroid.daemon.task.StartTask;
import org.transdroid.daemon.util.DLog;
import org.transdroid.daemon.util.HttpHelper;

import com.android.internalcopy.http.multipart.FilePart;
import com.android.internalcopy.http.multipart.MultipartEntity;
import com.android.internalcopy.http.multipart.Part;

/**
 * An adapter that allows for easy access to uTorrent torrent data. Communication
 * is handled via authenticated JSON-RPC HTTP GET requests and responses.
 * 
 * @author erickok
 *
 */
public class UtorrentAdapter implements IDaemonAdapter {

	private static final String LOG_NAME = "uTorrent daemon";
	private static final String RPC_URL_HASH = "&hash=";
	
	private DaemonSettings settings;
	private DefaultHttpClient httpclient;
	private String authtoken;

	/**
	 * Initialises an adapter that provides operations to the uTorrent web daemon
	 */
	public UtorrentAdapter(DaemonSettings settings) {
		this.settings = settings;
	}

	@Override
	public DaemonTaskResult executeTask(DaemonTask task) {
		
		try {
			switch (task.getMethod()) {
			case Retrieve:

				// Request all torrents from server
				JSONObject result = makeUtorrentRequest("&list=1");
				return new RetrieveTaskSuccessResult((RetrieveTask) task, parseJsonRetrieveTorrents(result.getJSONArray("torrents")),parseJsonRetrieveGetLabels(result.getJSONArray("label")));

			case GetTorrentDetails:
				
				// Request fine details of a specific torrent
				JSONObject dresult = makeUtorrentRequest("&action=getprops" + RPC_URL_HASH + task.getTargetTorrent().getUniqueID());
				return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, parseJsonTorrentDetails(dresult.getJSONArray("props")));
				
			case GetFileList:
				
				// Get the file listing of a torrent
				JSONObject files = makeUtorrentRequest("&action=getfiles" + RPC_URL_HASH + task.getTargetTorrent().getUniqueID());
				return new GetFileListTaskSuccessResult((GetFileListTask) task, parseJsonFileListing(files.getJSONArray("files").getJSONArray(1), task.getTargetTorrent()));
				
			case AddByFile:
				
				// Add a torrent to the server by sending the contents of a local .torrent file
				String file = ((AddByFileTask)task).getFile();
				uploadTorrentFile(file);
				return new DaemonTaskSuccessResult(task);

			case AddByUrl:

				// Request to add a torrent by URL
				String url = ((AddByUrlTask)task).getUrl();
				if (url == null || url.equals(""))
					throw new DaemonException(DaemonException.ExceptionType.ParsingFailed, "No url specified");
				makeUtorrentRequest("&action=add-url&s=" + URLEncoder.encode(url, "UTF-8"));
				return new DaemonTaskSuccessResult(task);

			case AddByMagnetUrl:

				// Request to add a magnet link by URL
				String magnet = ((AddByMagnetUrlTask)task).getUrl();
				makeUtorrentRequest("&action=add-url&s=" + URLEncoder.encode(magnet, "UTF-8"));
				return new DaemonTaskSuccessResult(task);

			case Remove:

				// Remove a torrent
				RemoveTask removeTask = (RemoveTask) task;
				if (removeTask.includingData()) {
					makeUtorrentRequest("&action=removedata" + RPC_URL_HASH + task.getTargetTorrent().getUniqueID());
				} else {
					makeUtorrentRequest("&action=remove" + RPC_URL_HASH + task.getTargetTorrent().getUniqueID());
				}
				return new DaemonTaskSuccessResult(task);
				
			case Pause:

				// Pause a torrent
				makeUtorrentRequest("&action=pause" + RPC_URL_HASH + task.getTargetTorrent().getUniqueID());
				return new DaemonTaskSuccessResult(task);
				
			case PauseAll:

				// Pause all torrents
				makeUtorrentRequest("&action=pause" + getAllHashes());
				return new DaemonTaskSuccessResult(task);

			case Resume:

				// Resume a torrent
				makeUtorrentRequest("&action=unpause" + RPC_URL_HASH + task.getTargetTorrent().getUniqueID());
				return new DaemonTaskSuccessResult(task);
				
			case ResumeAll:

				// Resume all torrents
				makeUtorrentRequest("&action=unpause" + getAllHashes());
				return new DaemonTaskSuccessResult(task);
				
			case Stop:
				
				// Stop a torrent
				makeUtorrentRequest("&action=stop" + RPC_URL_HASH + task.getTargetTorrent().getUniqueID());
				return new DaemonTaskSuccessResult(task);
				
			case StopAll:
				
				// Stop all torrents
				makeUtorrentRequest("&action=stop" + getAllHashes());
				return new DaemonTaskSuccessResult(task);
				
			case Start:

				// Start a torrent (maybe forced)
				StartTask startTask = (StartTask) task;
				if (startTask.isForced()) {
					makeUtorrentRequest("&action=forcestart" + RPC_URL_HASH + startTask.getTargetTorrent().getUniqueID());
				} else {
					makeUtorrentRequest("&action=start" + RPC_URL_HASH + startTask.getTargetTorrent().getUniqueID());
				}
				return new DaemonTaskSuccessResult(task);
				
			case StartAll:
				
				// Start all torrents
				makeUtorrentRequest("&action=start" + getAllHashes());
				return new DaemonTaskSuccessResult(task);
				
			case SetFilePriorities:
				
				// Set priorities of the files of some torrent
				SetFilePriorityTask prioTask = (SetFilePriorityTask) task;
				String prioUrl = "&p=" + convertPriority(prioTask.getNewPriority());
				for (TorrentFile forFile : prioTask.getForFiles()) {
					prioUrl += "&f=" + forFile.getKey();
				}
				makeUtorrentRequest("&action=setprio" + RPC_URL_HASH + task.getTargetTorrent().getUniqueID() + prioUrl);
				return new DaemonTaskSuccessResult(task);
				
			case SetTransferRates:

				// Request to set the maximum transfer rates
				SetTransferRatesTask ratesTask = (SetTransferRatesTask) task;
				makeUtorrentRequest( 
						"&action=setsetting&s=ul_auto_throttle&v=0&s=max_ul_rate&v=" + 
						(ratesTask.getUploadRate() == null? 0: ratesTask.getUploadRate().intValue()) + 
						"&s=max_dl_rate&v=" + 
						(ratesTask.getDownloadRate() == null? 0: ratesTask.getDownloadRate().intValue()));
				return new DaemonTaskSuccessResult(task);
				
			case SetLabel:
				
				// Set the label of some torrent
				SetLabelTask labelTask = (SetLabelTask) task;
				makeUtorrentRequest("&action=setprops" + RPC_URL_HASH + labelTask.getTargetTorrent().getUniqueID() + 
						"&s=label&v=" + URLEncoder.encode(labelTask.getNewLabel(), "UTF-8"));
				return new DaemonTaskSuccessResult(task);

			case SetTrackers:

				// Set the trackers of some torrent
				SetTrackersTask trackersTask = (SetTrackersTask) task;
				// Build list of tracker lines, separated by a \r\n
				String newTrackersText = "";
				for (String tracker : trackersTask.getNewTrackers()) {
					newTrackersText += (newTrackersText.length() == 0? "": "\r\n") + tracker;
				}
				makeUtorrentRequest("&action=setprops" + RPC_URL_HASH + trackersTask.getTargetTorrent().getUniqueID() + 
						"&s=trackers&v=" + URLEncoder.encode(newTrackersText, "UTF-8"));
				return new DaemonTaskSuccessResult(task);
			
			default:
				return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported, task.getMethod() + " is not supported by " + getType()));
			}
		} catch (JSONException e) {
			return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ParsingFailed, e.toString()));
		} catch (DaemonException e) {
			return new DaemonTaskFailureResult(task, e);
		} catch (FileNotFoundException e) {
			return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString()));
		} catch (UnsupportedEncodingException e) {
			return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported, e.toString()));
		} catch (IOException e) {
			return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ConnectionError, e.toString()));
		}
	}
	
	private static final int NAME_IDX = 0;
	private static final int COUNT_IDX = 1;

	private ArrayList<Label> parseJsonRetrieveGetLabels(JSONArray lresults) throws JSONException {

		// Parse response
		ArrayList<Label> labels = new ArrayList<Label>();
		for (int i = 0; i < lresults.length(); i++) {
			JSONArray lab = lresults.getJSONArray(i);
			String name = lab.getString(NAME_IDX);
			int count = lab.getInt(COUNT_IDX);
			labels.add(new Label(
					name,
					count
					));
		}
		return labels;
		
	}

	private JSONObject makeUtorrentRequest(String addToUrl) throws DaemonException {

		try {
				
			// Initialise the HTTP client
			if (httpclient == null) {
				initialise();
			}
			
			ensureToken();

			// Make request
			HttpGet httpget = new HttpGet(buildWebUIUrl() + "?token=" + authtoken + addToUrl);
			HttpResponse response = httpclient.execute(httpget);

			// Read JSON response
			InputStream instream = response.getEntity().getContent();
			String result = HttpHelper.ConvertStreamToString(instream);
			if ((result.equals("") || result.trim().equals("invalid request"))) {
				authtoken = null;
				throw new DaemonException(ExceptionType.AuthenticationFailure, "Response was '" + result.replace("\n", "") + "' instead of a proper JSON object (and we used auth token '" + authtoken + "')");
			}
			JSONObject json = new JSONObject(result);
			instream.close();
			return json;			
			
		} catch (DaemonException e) {
			throw e;
		} catch (JSONException e) {
			DLog.d(LOG_NAME, "Error: " + e.toString());
			throw new DaemonException(ExceptionType.ParsingFailed, e.toString());
		} catch (Exception e) {
			DLog.d(LOG_NAME, "Error: " + e.toString());
			throw new DaemonException(ExceptionType.ConnectionError, e.toString());
		}
		
	}

	private void ensureToken() throws IOException, ClientProtocolException, DaemonException {
		
		// Make sure we have a valid token
		if (authtoken == null) {
			
			// Make a request to /gui/token.html
			// See http://trac.utorrent.com/trac/wiki/TokenSystem
			HttpGet httpget = new HttpGet(buildWebUIUrl() + "token.html");
			
			// Parse the response HTML
			HttpResponse response = httpclient.execute(httpget);
			if (response.getStatusLine().getStatusCode() == 401) {
				throw new DaemonException(ExceptionType.AuthenticationFailure, "Auth denied (401) on token.html retrieval");
			}
			if (response.getStatusLine().getStatusCode() == 404) {
				throw new DaemonException(ExceptionType.ConnectionError, "Not found (404); server doesn't exist or is inaccessible");
			}
			InputStream instream = response.getEntity().getContent();
			String result = HttpHelper.ConvertStreamToString(instream);
			authtoken = result.replaceAll("\\<.*?>","").trim();

		}
		
	}

	public JSONObject uploadTorrentFile(String file) throws DaemonException, ClientProtocolException, IOException, JSONException {

		// Initialise the HTTP client
		if (httpclient == null) {
			initialise();
		}
		
		ensureToken();

		// Build and make request
		HttpPost httppost = new HttpPost(buildWebUIUrl() + "?token=" + authtoken + "&action=add-file");
		File upload = new File(URI.create(file));
		Part[] parts = { new FilePart("torrent_file", upload, FilePart.DEFAULT_CONTENT_TYPE, null) };
		httppost.setEntity(new MultipartEntity(parts, httppost.getParams()));
		HttpResponse response = httpclient.execute(httppost);

		// Read JSON response
		InputStream instream = response.getEntity().getContent();
		String result = HttpHelper.ConvertStreamToString(instream);
		JSONObject json = new JSONObject(result);
		instream.close();
		return json;
		
	}

	/**
	 * Instantiates an HTTP client with proper credentials that can be used for all Transmission requests.
	 * @param connectionTimeout The connection timeout in milliseconds
	 * @throws DaemonException On conflicting or missing settings
	 */
	private void initialise() throws DaemonException {
		this.httpclient = HttpHelper.createStandardHttpClient(settings, true);
	}
	
	/**
	 * Build the URL of the Transmission web UI from the user settings.
	 * @return The URL of the RPC API
	 */
	private String buildWebUIUrl() {
		return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + "/gui/";
	}

	private TorrentStatus convertUtorrentStatus(int uStatus, boolean finished) {
		// Convert bitwise int to uTorrent status codes
		// Now based on http://forum.utorrent.com/viewtopic.php?id=50779
		if ((uStatus & 1) == 1) {
			// Started
			if ((uStatus & 32) == 32) {
				// Paused
				return TorrentStatus.Paused;
			} else if (finished) {
				return TorrentStatus.Seeding;
			} else {
				return TorrentStatus.Downloading;
			}
		} else if ((uStatus & 2) == 2) {
			// Checking
			return TorrentStatus.Checking;
		} else if ((uStatus & 16) == 16) {
			// Error
			return TorrentStatus.Error;
		} else if ((uStatus & 128) == 128) {
			// Queued
			return TorrentStatus.Queued;
		} else {
			return TorrentStatus.Waiting;
		}
	}
	
	private Priority convertUtorrentPriority(int code) {
		switch (code) {
		case 0:
			return Priority.Off;
		case 1:
			return Priority.Low;
		case 3:
			return Priority.High;
		default:
			return Priority.Normal;	
		}
	}

	private int convertPriority(Priority newPriority) {
		if (newPriority == null) {
			return 2; 
		}
		switch (newPriority) {
		case Off:
			return 0;
		case Low:
			return 1;
		case High:
			return 3;
		default:
			return 2;
		}
	}

	// These are the positions inside the JSON response array of a torrent
	// See http://forum.utorrent.com/viewtopic.php?id=25661
	private static final int RPC_HASH_IDX = 0;
	private static final int RPC_STATUS_IDX = 1;
	private static final int RPC_NAME_IDX = 2;
	private static final int RPC_SIZE_IDX = 3;
	private static final int RPC_PARTDONE = 4;
	private static final int RPC_DOWNLOADED_IDX = 5;
	private static final int RPC_UPLOADED_IDX = 6;
	private static final int RPC_DOWNLOADSPEED_IDX = 9;
	private static final int RPC_UPLOADSPEED_IDX = 8;
	private static final int RPC_ETA_IDX = 10;
	private static final int RPC_LABEL_IDX = 11;
	private static final int RPC_PEERSCONNECTED_IDX = 12;
	private static final int RPC_PEERSINSWARM_IDX = 13;
	private static final int RPC_SEEDSCONNECTED_IDX = 14;
	private static final int RPC_SEEDSINSWARM_IDX = 15;
	private static final int RPC_AVAILABILITY_IDX = 16;
	private static final int RPC_ADDEDON_IDX = 23;
	private static final int RPC_COMPLETEDON_IDX = 24;

	private ArrayList<Torrent> parseJsonRetrieveTorrents(JSONArray results) throws JSONException {

		// Parse response
		ArrayList<Torrent> torrents = new ArrayList<Torrent>();
		boolean createPaths = !(settings.getDownloadDir() == null || settings.getDownloadDir().equals(""));
		for (int i = 0; i < results.length(); i++) {
			JSONArray tor = results.getJSONArray(i);
			String name = tor.getString(RPC_NAME_IDX);
			boolean downloaded = (tor.getLong(RPC_PARTDONE) == 1000l);
			float available = ((float)tor.getInt(RPC_AVAILABILITY_IDX)) / 65536f; // Integer in 1/65536ths
			// The full torrent path is not available in uTorrent web UI API
			// Guess the torrent's directory based on the user-specific default download dir and the torrent name
			String dir = null;
			if (createPaths) {
				dir = settings.getDownloadDir();
				if (name.length() < 4 || name.charAt(name.length() - 4) != '.') {
					// Assume this is a directory rather than a single-file torrent
					dir += name + settings.getOS().getPathSeperator();
				}
			}
			// Add the parsed torrent to the list
			TorrentStatus status = convertUtorrentStatus(tor.getInt(RPC_STATUS_IDX), downloaded);
			long addedOn = tor.optInt(RPC_ADDEDON_IDX, -1) * 1000L;
			long completedOn = tor.optInt(RPC_COMPLETEDON_IDX, -1) * 100L;
			Date addedOnDate = addedOn == -1? null: new Date(addedOn);
			Date completedOnDate = completedOn == -1? null: new Date(completedOn);
			torrents.add(new Torrent(
					i, // No ID but a hash is used
					tor.getString(RPC_HASH_IDX),
					name,
					status,
					dir,
					tor.getInt(RPC_DOWNLOADSPEED_IDX),
					tor.getInt(RPC_UPLOADSPEED_IDX),
					tor.getInt(RPC_SEEDSCONNECTED_IDX),
					tor.getInt(RPC_PEERSCONNECTED_IDX),
					(downloaded? tor.getInt(RPC_SEEDSINSWARM_IDX): tor.getInt(RPC_PEERSINSWARM_IDX)),
					0, // Not available
					tor.getInt(RPC_ETA_IDX),
					tor.getLong(RPC_DOWNLOADED_IDX),
					tor.getLong(RPC_UPLOADED_IDX),
					tor.getLong(RPC_SIZE_IDX),
					((float) tor.getLong(RPC_PARTDONE)) / 1000f, // Integer in promille
					Math.min(available, 1f), // Can be > 100% if multiple peers have 100%
					tor.getString(RPC_LABEL_IDX).trim(),
					addedOnDate,
					completedOnDate,
					// uTorrent doesn't give the error message, so just remind that there is some error
					status == TorrentStatus.Error? "See GUI for error message": null));
		}
		return torrents;
		
	}

	private TorrentDetails parseJsonTorrentDetails(JSONArray results) throws JSONException {

		// Parse response
		// NOTE: Assumes only details for one torrent are requested at a time
		if (results.length() > 0) {
			
			JSONObject tor = results.getJSONObject(0);
			List<String> trackers = new ArrayList<String>();
			for (String tracker : tor.getString("trackers").split("\\r\\n")) {
				// Ignore any blank lines
				if (!tracker.trim().equals("")) {
					trackers.add(tracker.trim());
				}
			}
			// uTorrent doesn't support tracker error messages in the web UI
			// See http://forum.utorrent.com/viewtopic.php?pid=553340#p553340
			return new TorrentDetails(trackers, null);
		}
		
		return null;
		
	}

	// These are the positions inside the JSON response array of a torrent
	// See http://forum.utorrent.com/viewtopic.php?id=25661
	private static final int RPC_FILENAME_IDX = 0;
	private static final int RPC_FILESIZE_IDX = 1;
	private static final int RPC_FILEDOWNLOADED_IDX = 2;
	private static final int RPC_FILEPRIORITY_IDX = 3;

	private ArrayList<TorrentFile> parseJsonFileListing(JSONArray results, Torrent torrent) throws JSONException {

		// Parse response
		ArrayList<TorrentFile> files = new ArrayList<TorrentFile>();
		boolean createPaths = torrent != null && torrent.getLocationDir() != null && !torrent.getLocationDir().equals("");
		final String pathSep = settings.getOS().getPathSeperator();
		for (int i = 0; i < results.length(); i++) {
			JSONArray file = results.getJSONArray(i);
			// Add the parsed torrent to the list
			files.add(new TorrentFile(
					"" + i,
					file.getString(RPC_FILENAME_IDX),		// Name
					(createPaths? file.getString(RPC_FILENAME_IDX).replace((pathSep.equals("/")? "\\": "/"), pathSep): null),	// Relative path; 'wrong' path slashes will be replaced
					(createPaths? torrent.getLocationDir() + file.getString(RPC_FILENAME_IDX).replace((pathSep.equals("/")? "\\": "/"), pathSep): null),	// Full path; 'wrong' path slashes will be replaced
					file.getLong(RPC_FILESIZE_IDX),			// Total size
					file.getLong(RPC_FILEDOWNLOADED_IDX),	// Part done
					convertUtorrentPriority(file.getInt(RPC_FILEPRIORITY_IDX))));	// Priority
		}
		return files;
		
	}
	
	private String getAllHashes() throws DaemonException, JSONException {
		
		// Make a retrieve torrents call first to gather all hashes
		JSONObject result = makeUtorrentRequest("&list=1");
		ArrayList<Torrent> torrents = parseJsonRetrieveTorrents(result.getJSONArray("torrents"));
		
		// Build a string of hashes of all the torrents
		String hashes = "";
		for (Torrent torrent : torrents) {
			hashes += RPC_URL_HASH + torrent.getUniqueID();
		}
		return hashes;
		
	}

	@Override
	public Daemon getType() {
		return settings.getType();
	}

	@Override
	public DaemonSettings getSettings() {
		return this.settings;
	}
		
}
